﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Graphics.PackedVector;
using ShevaEngine.Core.Modules.Models;
using ShevaEngine.Core.Modules.Models.Pipeline;
using ShevaEngine2.Modules.Animations;
using ShevaEngine2ContentPipeline.Common;

namespace ShevaEngine2ContentPipeline.Processors
{
    /// <summary>
    /// Sheva model processor.
    /// </summary>
    [ContentProcessor(DisplayName = "ShevaModelProcessor")]
    public class ShevaModelProcessor : ContentProcessor<NodeContent, ModelInternal>
    {
        /// <summary>Scale.</summary>        
        public Vector3 Scale { get; set; }
        /// <summary>Rotation Y.</summary>
        public float RotationY { get; set; }
        /// <summary>Scale.</summary>
        [DefaultValue(Axe.Y)]
        public Axe UpAxe { get; set; }
        
        /// <summary>Context.</summary>
        private ContentProcessorContext Context;
        /// <summary>Buffers.</summary>
        private List<ModelInternalBuffer> Buffers;
        /// <summary>VertexDeclaration to Buffer map.</summary>
        public Dictionary<VertexDeclarationContent, int> VDBuffers;
        /// <summary>Temp indices.</summary>
        private static int[] TempIndices = new int[4];
        /// <summary>Temp weights.</summary>
        private static float[] TempWeights = new float[4];

        /// <summary>
        /// Constructor.
        /// </summary>
        public ShevaModelProcessor()
        {
            this.Scale = new Vector3(1, 1, 1);
            this.UpAxe = Axe.Y;
        }

        /// <summary>
        /// Method process.
        /// </summary>
        /// <param name="input"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public override ModelInternal Process(NodeContent input, ContentProcessorContext context)
        {
            this.Context = context;

            ModelInternal output = new ModelInternal();

            Matrix completeMatrix = Matrix.Identity;

            if (this.UpAxe == Axe.Z)
                completeMatrix = Matrix.CreateRotationX(MathHelper.PiOver2) * Matrix.CreateRotationZ(MathHelper.Pi);

            MeshHelper.TransformScene(input, completeMatrix * 
                Matrix.CreateScale(this.Scale) * Matrix.CreateRotationY(MathHelper.ToRadians(this.RotationY)));            

            this.Buffers = new List<ModelInternalBuffer>();
            this.VDBuffers = new Dictionary<VertexDeclarationContent, int>(new VertexDeclarationEqualityComparer());            

            BoneContent skeleton = MeshHelper.FindSkeleton(input);           

            if (skeleton != null)
            {
                this.FlattenTransforms(input, skeleton);

                output.SkinningData = this.ProcessSkeleton(skeleton);
            }

            this.ProcessMeshes(output, input);

            output.Buffers = this.Buffers;            

            return output;
        }

        /// <summary>
        /// Method process meshes.
        /// </summary>
        /// <param name="mesh"></param>
        private void ProcessMeshes(ModelInternal modelNode, NodeContent node)
        {
            ModelInternalNode newModelNode = null;

            if (node is MeshContent)
            {
                MeshContent mesh = node as MeshContent;
                
                if (mesh.Geometry[0].Vertices.Channels.Contains("TextureCoordinate0") &&
                    !mesh.Geometry[0].Vertices.Channels.Contains("Tangent0"))
                    MeshHelper.CalculateTangentFrames(mesh,
                        VertexChannelNames.TextureCoordinate(0),
                        VertexChannelNames.Tangent(0),
                        VertexChannelNames.Binormal(0));

                ModelInternalNodeTransform newTransform = new ModelInternalNodeTransform();
                newTransform.Transform = mesh.Transform;

                modelNode.Add(newTransform);

                newModelNode = newTransform;

                this.ProcessGeometries(newModelNode, mesh);
            }

            if (newModelNode == null)    
                foreach (NodeContent item in node.Children)
                    this.ProcessMeshes(modelNode, item);
            else
                foreach (NodeContent item in node.Children)
                    this.ProcessMeshes(newModelNode, item);
        }

        /// <summary>
        /// Method process meshes.
        /// </summary>
        /// <param name="mesh"></param>
        private void ProcessMeshes(ModelInternalNode modelNode, NodeContent node)
        {
            ModelInternalNode newModelNode = modelNode;

            if (node is MeshContent)
            {
                MeshContent mesh = node as MeshContent;
                
                MeshHelper.CalculateNormals(mesh, false);

                if (mesh.Geometry[0].Vertices.Channels.Contains("TextureCoordinate0") &&
                    !mesh.Geometry[0].Vertices.Channels.Contains("Tangent0"))
                    MeshHelper.CalculateTangentFrames(mesh,
                        VertexChannelNames.TextureCoordinate(0),
                        VertexChannelNames.Tangent(0),
                        VertexChannelNames.Binormal(0));

                ModelInternalNodeTransform newTransform = new ModelInternalNodeTransform();
                newTransform.Transform = mesh.Transform;
                
                modelNode.Add(newTransform);
                
                newModelNode = newTransform;

                this.ProcessGeometries(newModelNode, mesh);
            }
            
            foreach (NodeContent item in node.Children)
                this.ProcessMeshes(newModelNode, item);
        }

        /// <summary>
        /// Methods process geometries.
        /// </summary>
        /// <param name="modelNode"></param>
        /// <param name="mesh"></param>
        private void ProcessGeometries(ModelInternalNode modelNode, MeshContent mesh)
        {
            foreach (GeometryContent geometry in mesh.Geometry)
                if (geometry.Indices.Count != 0)
                {                    
                    ModelInternalNodeGeometry newGeometry = new ModelInternalNodeGeometry();

                    bool animated = this.ProcessVertexChannels(geometry, this.Context);

                    newGeometry.MaterialParameters = this.ProcessMaterialInfo(geometry, animated);

                    newGeometry.NumVertices = geometry.Vertices.VertexCount;
                    newGeometry.PrimitiveCount = geometry.Indices.Count / 3;
                    newGeometry.PrimitiveType = PrimitiveType.TriangleList;
                    newGeometry.BoundingSphere = BoundingSphere.CreateFromPoints(geometry.Vertices.Positions);

                    VertexBufferContent vboContent = geometry.Vertices.CreateVertexBuffer();
                    
                    VertexElement[] elements = new VertexElement[vboContent.VertexDeclaration.VertexElements.Count];
                    for (int i = 0; i < vboContent.VertexDeclaration.VertexElements.Count; i++)
                        elements[i] = vboContent.VertexDeclaration.VertexElements[i];

                    if (!this.VDBuffers.ContainsKey(vboContent.VertexDeclaration))
                    {
                        this.VDBuffers.Add(vboContent.VertexDeclaration, this.VDBuffers.Count);
                        this.Buffers.Add(new ModelInternalBuffer());
                        this.Buffers[this.Buffers.Count - 1].VertexElements = elements;
                        this.Buffers[this.Buffers.Count - 1].ElementSize = IndexElementSize.ThirtyTwoBits;
                        this.Buffers[this.Buffers.Count - 1].VertexStride = vboContent.VertexDeclaration.VertexStride;
                    }

                    newGeometry.BuffersIndex = this.VDBuffers[vboContent.VertexDeclaration];

                    newGeometry.BaseVertex = this.Buffers[newGeometry.BuffersIndex].VerticesCount;

                    this.Buffers[newGeometry.BuffersIndex].VerticesCount += geometry.Vertices.VertexCount;
                    this.Buffers[newGeometry.BuffersIndex].VertexBufferContent.AddRange(vboContent.VertexData);

                    newGeometry.StartIndex = this.Buffers[newGeometry.BuffersIndex].IndicesCount;

                    foreach (int item in geometry.Indices)
                        this.Buffers[newGeometry.BuffersIndex].IndexBufferContent.AddRange(BitConverter.GetBytes((int)item));

                    this.Buffers[newGeometry.BuffersIndex].IndicesCount += geometry.Indices.Count;

                    modelNode.Add(newGeometry);
                }
        }

        /// <summary>
        /// Method process material info.
        /// </summary>
        /// <param name="geometry"></param>
        /// <returns></returns>
        private ModelMaterialParameters ProcessMaterialInfo(GeometryContent geometry, bool animated)
        {
            ModelMaterialParameters output = new ModelMaterialParameters();

            if (geometry.Material != null)
            {
                try
                {
                    output.Name = geometry.Material.Name.Split('_')[0];

                    if (output.Name != "MaterialNormalMap")
                        if (animated)
                            output.Name = "MaterialNormalMapAnimated";
                        else
                            output.Name = "MaterialNormalMap";
                }
                catch
                {
                    if (animated)
                        output.Name = "MaterialNormalMapAnimated";
                    else
                        output.Name = "MaterialNormalMap";
                }                

                foreach (string key in geometry.Material.OpaqueData.Keys)
                    switch (key)
                    {
                        case "Name":
                            output.Name = (string)geometry.Material.OpaqueData[key];
                            break;
                        case "DiffuseColor":
                            output.Diffuse = new Color((Vector3)geometry.Material.OpaqueData[key]);
                            break;
                        case "EmissiveColor":
                            output.Emissive = new Color((Vector3)geometry.Material.OpaqueData[key]);
                            break;
                        case "SpecularColor":
                            output.Specular = new Color((Vector3)geometry.Material.OpaqueData[key]);
                            break;
                        case "SpecularPower":
                            output.SpecularPower = (float)geometry.Material.OpaqueData[key];
                            break;
                        case "Alpha":
                            output.Alpha = (float)geometry.Material.OpaqueData[key];
                            break;
                        default:
                            break;
                    }

                foreach (KeyValuePair<string, ExternalReference<TextureContent>> item in geometry.Material.Textures)
                {
                    //newGeometry.MaterialParameters.TexturesTemp.Add(item.Key, Path.GetFileNameWithoutExtension(item.Value.Filename));

                    switch (item.Key)
                    {
                        case "Texture":
                            output.Textures.Add("Diffuse", Path.GetFileNameWithoutExtension(item.Value.Filename));
                            break;
                        case "SpecularFactor":
                            output.Textures.Add("Specular", Path.GetFileNameWithoutExtension(item.Value.Filename));
                            break;
                        case "Reflection":
                            //newGeometry.MaterialParameters.TexturesTemp.Add("Reflection", Path.GetFileNameWithoutExtension(item.Value.Filename));
                            break;
                        case "Bump":
                            output.Textures.Add("Normal", Path.GetFileNameWithoutExtension(item.Value.Filename));
                            break;
                        case "Transparency":
                            break;
                        default:
                            break;
                    }
                }

                if (output.Name.Contains("NormalMap") && 
                    output.Textures.ContainsKey("Diffuse") &&
                    !output.Textures.ContainsKey("Normal"))
                    output.Textures.Add("Normal", output.Textures["Diffuse"].Replace("colormap", "normalmap"));
            }

            return output;
        }

        /// <summary>
        /// Process vertex channels - XNA.dll.
        /// </summary>
        /// <param name="geometry"></param>
        /// <param name="context"></param>
        protected bool ProcessVertexChannels(GeometryContent geometry, ContentProcessorContext context)
        {
            bool output = false;

            for (int vertexChannelIndex = 0; vertexChannelIndex < geometry.Vertices.Channels.Count; vertexChannelIndex++)
            {
                string str = VertexChannelNames.DecodeBaseName(geometry.Vertices.Channels[vertexChannelIndex].Name);

                if (str == "Weights")
                {
                    ProcessWeightsChannel(geometry, vertexChannelIndex);

                    output = true;
                }

                //{
                //    if (!(str == "Color"))
                //    {
                //        if (!(str == "Weights"))
                //        {
                //            return;
                //        }
                //    }
                //    else
                //    {
                //        //this.ProcessColorChannel(geometry, vertexChannelIndex);
                //        return;
                //    }
                //    ProcessWeightsChannel(geometry, vertexChannelIndex);
                //}
            }

            return output;
        }

        /// <summary>
        /// Process weights channel - XNA.dll.
        /// </summary>
        /// <param name="geometry"></param>
        /// <param name="vertexChannelIndex"></param>
        private static void ProcessWeightsChannel(GeometryContent geometry, int vertexChannelIndex)
        {
            BoneContent skeleton = MeshHelper.FindSkeleton(geometry.Parent);
            if (skeleton == null)
            {
                throw new InvalidContentException("SkeletonNotFound", geometry.Parent.Identity);
            }
            Dictionary<string, int> boneIndices = FlattenSkeleton(skeleton);
            VertexChannelCollection channels = geometry.Vertices.Channels;
            VertexChannel channel2 = channels[vertexChannelIndex];
            VertexChannel<BoneWeightCollection> channel = channel2 as VertexChannel<BoneWeightCollection>;
            if (channel == null)
            {
                throw new InvalidContentException(string.Format(CultureInfo.CurrentCulture, "VertexChannelWrongContentType", new object[] { channel2.Name, channel2.ElementType, typeof(BoneWeightCollection) }));
            }
            Byte4[] outputIndices = new Byte4[channel.Count];
            Vector4[] outputWeights = new Vector4[channel.Count];
            for (int i = 0; i < channel.Count; i++)
            {
                BoneWeightCollection inputWeights = channel[i];
                if (inputWeights == null)
                {
                    throw new InvalidContentException(string.Format(CultureInfo.CurrentCulture, "NullVertexChannelEntry", new object[] { channel.Name, typeof(BoneWeightCollection).Name }));
                }
                ConvertVertexWeights(inputWeights, boneIndices, outputIndices, outputWeights, i, geometry);
            }
            int usageIndex = VertexChannelNames.DecodeUsageIndex(channel.Name);
            string name = VertexChannelNames.EncodeName(VertexElementUsage.BlendIndices, usageIndex);
            string str = VertexChannelNames.EncodeName(VertexElementUsage.BlendWeight, usageIndex);
            if (channels.Contains(name))
            {
                throw new InvalidContentException(string.Format(CultureInfo.CurrentCulture, "ConvertWeightsOutputAlreadyExists", new object[] { channel.Name, name }));
            }
            if (channels.Contains(str))
            {
                throw new InvalidContentException(string.Format(CultureInfo.CurrentCulture, "ConvertWeightsOutputAlreadyExists", new object[] { channel.Name, str }));
            }
            channels.Insert<Byte4>(vertexChannelIndex + 1, name, outputIndices);
            channels.Insert<Vector4>(vertexChannelIndex + 2, str, outputWeights);
            channels.RemoveAt(vertexChannelIndex);
        }

        /// <summary>
        /// Convert vertex weights - XNA.dll.
        /// </summary>
        /// <param name="inputWeights"></param>
        /// <param name="boneIndices"></param>
        /// <param name="outputIndices"></param>
        /// <param name="outputWeights"></param>
        /// <param name="vertexIndex"></param>
        /// <param name="geometry"></param>
        private static void ConvertVertexWeights(BoneWeightCollection inputWeights, Dictionary<string, int> boneIndices, Byte4[] outputIndices, Vector4[] outputWeights, int vertexIndex, GeometryContent geometry)
        {
            inputWeights.NormalizeWeights(4);
            for (int i = 0; i < inputWeights.Count; i++)
            {
                BoneWeight weight = inputWeights[i];
                if (!boneIndices.TryGetValue(weight.BoneName, out TempIndices[i]))
                {
                    throw new InvalidContentException(string.Format(CultureInfo.CurrentCulture, "VertexHasUnknownBoneName", new object[] { weight.BoneName }), geometry.Parent.Identity);
                }
                TempWeights[i] = weight.Weight;
            }
            for (int j = inputWeights.Count; j < 4; j++)
            {
                TempIndices[j] = 0;
                TempWeights[j] = 0f;
            }
            outputIndices[vertexIndex] = new Byte4((float)TempIndices[0], (float)TempIndices[1], (float)TempIndices[2], (float)TempIndices[3]);
            outputWeights[vertexIndex] = new Vector4(TempWeights[0], TempWeights[1], TempWeights[2], TempWeights[3]);
        }


        /// <summary>
        /// Flatten skeleton  -  XNA.dll.
        /// </summary>
        /// <param name="skeleton"></param>
        /// <returns></returns>
        private static Dictionary<string, int> FlattenSkeleton(BoneContent skeleton)
        {
            Dictionary<string, int> dictionary = new Dictionary<string, int>();
            IList<BoneContent> list = MeshHelper.FlattenSkeleton(skeleton);
            if (list.Count > 0xff)
            {
                throw new InvalidContentException(string.Format(CultureInfo.CurrentCulture, "TooManyBones", new object[] { list.Count, (byte)0xff }), skeleton.Identity);
            }
            for (int i = 0; i < list.Count; i++)
            {
                BoneContent content = list[i];
                if (!string.IsNullOrEmpty(content.Name))
                {
                    if (dictionary.ContainsKey(content.Name))
                    {
                        throw new InvalidContentException(string.Format(CultureInfo.CurrentCulture, "DuplicateBoneName", new object[] { content.Name }), skeleton.Identity);
                    }
                    dictionary.Add(content.Name, i);
                }
            }
            return dictionary;
        }


        private SkinningData ProcessSkeleton(BoneContent skeleton)
        {
            // Read the bind pose and skeleton hierarchy data.
            IList<BoneContent> bones = MeshHelper.FlattenSkeleton(skeleton);

            if (bones.Count > SkinnedEffect.MaxBones)
            {
                throw new InvalidContentException(string.Format(
                    "Skeleton has {0} bones, but the maximum supported is {1}.",
                    bones.Count, SkinnedEffect.MaxBones));
            }

            List<Matrix> bindPose = new List<Matrix>();
            List<Matrix> inverseBindPose = new List<Matrix>();
            List<int> skeletonHierarchy = new List<int>();
            Dictionary<string, int> boneIndices = new Dictionary<string, int>();

            foreach (BoneContent bone in bones)
            {
                bindPose.Add(bone.Transform);
                inverseBindPose.Add(Matrix.Invert(bone.AbsoluteTransform));
                skeletonHierarchy.Add(bones.IndexOf(bone.Parent as BoneContent));
                boneIndices.Add(bone.Name, boneIndices.Count);
            }

            // Convert animation data to our runtime format.
            Dictionary<string, AnimationClip> animationClips;
            animationClips = this.ProcessAnimations(skeleton.Animations, bones);

            return new SkinningData(animationClips, bindPose, inverseBindPose, skeletonHierarchy, boneIndices);
        }

        /// <summary>
        /// Converts an intermediate format content pipeline AnimationContentDictionary
        /// object to our runtime AnimationClip format.
        /// </summary>
        private Dictionary<string, AnimationClip> ProcessAnimations(
            AnimationContentDictionary animations, IList<BoneContent> bones)
        {
            // Build up a table mapping bone names to indices.
            Dictionary<string, int> boneMap = new Dictionary<string, int>();

            for (int i = 0; i < bones.Count; i++)
            {
                string boneName = bones[i].Name;

                if (!string.IsNullOrEmpty(boneName))
                    boneMap.Add(boneName, i);
            }

            // Convert each animation in turn.
            Dictionary<string, AnimationClip> animationClips;
            animationClips = new Dictionary<string, AnimationClip>();

            foreach (KeyValuePair<string, AnimationContent> animation in animations)
            {
                AnimationClip processed = this.ProcessAnimation(animation.Value, boneMap);

                animationClips.Add(animation.Key, processed);
            }

            if (animationClips.Count == 0)
            {
                throw new InvalidContentException(
                            "Input file does not contain any animations.");
            }

            return animationClips;
        }


        /// <summary>
        /// Converts an intermediate format content pipeline AnimationContent
        /// object to our runtime AnimationClip format.
        /// </summary>
        private AnimationClip ProcessAnimation(AnimationContent animation,
                                              Dictionary<string, int> boneMap)
        {
            List<Keyframe> keyframes = new List<Keyframe>();

            // For each input animation channel.
            foreach (KeyValuePair<string, AnimationChannel> channel in
                animation.Channels)
            {
                // Look up what bone this channel is controlling.
                int boneIndex;

                if (!boneMap.TryGetValue(channel.Key, out boneIndex))
                {
                    throw new InvalidContentException(string.Format(
                        "Found animation for bone '{0}', " +
                        "which is not part of the skeleton.", channel.Key));
                }

                // Convert the keyframe data.
                foreach (AnimationKeyframe keyframe in channel.Value)
                {
                    keyframes.Add(new Keyframe(boneIndex, keyframe.Time,
                                               keyframe.Transform));
                }
            }

            // Sort the merged keyframes by time.
            keyframes.Sort(ShevaModelProcessor.CompareKeyframeTimes);

            if (keyframes.Count == 0)
                throw new InvalidContentException("Animation has no keyframes.");

            if (animation.Duration <= TimeSpan.Zero)
                throw new InvalidContentException("Animation has a zero duration.");

            return new AnimationClip(animation.Duration, keyframes);
        }

        /// <summary>
        /// Comparison function for sorting keyframes into ascending time order.
        /// </summary>
        static int CompareKeyframeTimes(Keyframe a, Keyframe b)
        {
            return a.Time.CompareTo(b.Time);
        }

        /// <summary>
        /// Bakes unwanted transforms into the model geometry,
        /// so everything ends up in the same coordinate system.
        /// </summary>
        private void FlattenTransforms(NodeContent node, BoneContent skeleton)
        {
            foreach (NodeContent child in node.Children)
            {
                // Don't process the skeleton, because that is special.
                if (child == skeleton)
                    continue;

                // Bake the local transform into the actual geometry.
                MeshHelper.TransformScene(child, child.Transform);

                // Having baked it, we can now set the local
                // coordinate system back to identity.
                child.Transform = Matrix.Identity;

                // Recurse.
                FlattenTransforms(child, skeleton);
            }
        }
    }
}